fix: make an auth failure a first-class outcome, not report text - #601
Merged
Conversation
hyoshi
force-pushed
the
fix/daily-check-auth-failure
branch
from
August 12, 2026 21:51
7ce2122 to
bb88688
Compare
When a platform's credentials were missing or its token had expired,
/daily-check ran to completion and shipped a report that looked
complete: the affected section carried error prose where numbers
belonged, next to real figures from the platforms that did answer,
followed by the usual recommendations. A reader skimming it concluded
the platform had been quiet rather than unreadable.
The cause was that an auth failure arrived as an ordinary *successful*
tool result whose text happened to be a sentence about credentials --
"Credentials not found. Set environment variable ..." from
_no_creds_result, or "API error: ..." from api_error_handler flattening
an expired token into the same untyped string as a quota error. Nothing
downstream could tell "no spend" from "could not read".
Auth failure now has one machine-readable envelope on every platform:
{"status": "auth_error", "auth_cause": "no_credentials" |
"token_invalid", "detail": ...}. It reuses the status-field convention
blind_spots and DeliveryCollapseReport already use for exactly this
"what did I fail to see" question rather than adding a second one, and
detail keeps the operator-facing sentence so nothing readable is lost.
The two causes are separated because their recovery differs: configure
the credential vs. re-authorize a rejected one.
It is produced in the two places every platform routes through, so
Google Ads, Meta Ads and Search Console behave identically:
_no_creds_result for a missing credential, and api_error_handler for a
rejected one -- an HTTP 401/403, a Google Ads authentication_error /
authorization_error, or a PlatformAuthError. Meta answers an expired
token with HTTP 400 and an OAuthException body, so the Meta client now
names that case explicitly instead of letting it flatten. The
classifier is deliberately narrow: an unrecognized failure stays an
ordinary "API error:", because mislabelling a quota or validation
failure would send an operator to re-authorize a healthy account and
would withhold a report section that had good data behind it.
/daily-check gained the branch it was missing. On an auth_error it
marks the report partial in its opening line, names each affected
platform with its cause and recovery, withholds every verdict,
goal-progress line and recommendation that depends on the missing
platform's data, and still completes for every platform that did
answer. The #440 rule -- never fail the whole daily-check because one
platform broke -- is untouched: the report degrades, it does not stop.
The shared _mureo-shared reference, which documented an envelope shape
mureo never actually returned, now documents the real one.
is_error_result recognises the new envelope too, so a mutation refused
for a missing or rejected credential is still kept out of action_log;
without that it would have been recorded as a change that never
happened, complete with an observation_due and a reversal plan.
Closes #580
hyoshi
force-pushed
the
fix/daily-check-auth-failure
branch
from
August 12, 2026 23:02
bb88688 to
f53aa5b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
A platform's credentials are missing or its token has expired.
/daily-checkruns to completion and ships a report that looks complete: the affected section carries error prose where numbers should be —"API error: Meta API request failed (status=400, ...)"or"Credentials not found. Set environment variable ..."— sitting next to real figures from the platforms that did answer, followed by the usual recommendations. A reader skimming it concludes the platform was quiet rather than unreadable.The cause is that an auth failure arrives as an ordinary successful tool result whose text happens to be a sentence about credentials.
_no_creds_resultreturns the sentence as the whole body;api_error_handler's blanketexcept Exceptionflattens an expired token intof"{API_ERROR_PREFIX} {exc}". Both are prose, so nothing downstream can tell "no spend" from "could not read" — and no read path consulted the one detector that exists (is_error_result, wired only to mutation/action-log gating).The mechanism chosen
One machine-readable envelope, on every platform:
{"status": "auth_error", "auth_cause": "no_credentials", "detail": "Credentials not found. Set environment variable (META_ADS_ACCESS_TOKEN) or configure the credentials file."}Why this shape:
blind_spots/ChangeImportStatus(change-import) andDeliveryCollapseReport.status(no_credentials/data_unavailable) already answer the "what did I fail to see" question with astatusfield and a small vocabulary. A skill that has learned one has learned this.Tools return structured JSON data onlyis a design constraint, and the failure being fixed is precisely that prose was indistinguishable from prose.detailkeeps the operator-facing sentence, so nothing readable is lost — it is a field now, never the whole answer.no_credentials= nothing configured (mureo configure/mureo auth setup);token_invalid= a credential exists and the platform rejected it (re-authorize). The issue noted that missing / expired / transient were all the same untyped string; transient stays an ordinaryAPI error:.Produced in the two places every platform routes through, so Google Ads, Meta Ads and Search Console behave identically rather than by three separate edits:
_no_creds_resultno_credentialsapi_error_handlertoken_invalidauthentication_error/authorization_error,PlatformAuthErrorMeta needed one raise-site change: it answers an expired token with HTTP 400 and an
OAuthExceptionbody, so the status code alone cannot tell a dead credential from a bad request.mureo/meta_ads/client.pynow names that case (type == "OAuthException", or code 190 / 102, or a 401) and raisesPlatformAuthError.Classification is deliberately narrow, and unrecognized failures fall through to the existing
API error:envelope. Over-claiming is the expensive direction: mislabelling a quota or validation error as an auth failure would send an operator to re-authorize a healthy account and withhold a report section that had perfectly good data behind it.The skill branch
skills/daily-check/SKILL.mdhad no auth-failure branch — every failure-handling line in it addresses a different condition and says to keep going (the analytics-module fall-through, the official-hosted-MCP tool-surface fallbacks, step 2b'sblind_spots). Two additions:auth_errorresult is a hole in the report, not data: never renderdetailwhere a metric belongs, never read that platform's missing figures as "quiet", carry the platform +auth_causeforward exactly as step 2b'sblind_spotsare carried, and keep going for every other platform.auth_error, the report opens by saying it is partial, names each affected platform with its cause and recovery, and withholds every verdict, goal-progress line and recommendation that depends on that platform's numbers.The #440 rule (
never fail the whole daily-check because one platform's module errored) is untouched: the report degrades, it does not stop.skills/_mureo-shared/SKILL.mddocumented an "Authentication Error" shape mureo never actually returned ({"error": "No credentials found..."}). It now documents the real envelope, so every skill that reads the shared prerequisite can see the marker.Both copies of both skills are byte-identical (enforced by
test_copies_are_byte_identicalin the new suite and the existing one).Not-obviously-related change that is load-bearing
is_error_resultnow recognises the auth envelope too. Without it, a mutation refused for a missing or rejected credential would stop matching the mutation gate and be promoted intoaction_logas a change that never happened — complete with anobservation_dueand a reversal plan for it. (Ano_credentialsmutation was already slipping past that gate before this PR, since the sentence never carried theAPI error:prefix.)Corrections to the issue's trace
Verified each cited file:line against current
main. Accurate:_handlers_meta_ads.py:37-41,_helpers.py:166-168/:175/:178-191/:210-212,meta_ads/client.py:312-315,SKILL.md:59,SKILL.md:46,native_reversal.py:342-351/:373,rollback/executor.py:135/:185,exclusion_preflight.py:293-295,amazon_ads/bridge.py:202. Off (they point at an older revision of the file):skills/daily-check/SKILL.mdis 151 lines, not 143.:51/:52(step 3) and:69/:70(step 4), not:62/:63—:62/:63are the delivery-collapse block._no_creds_resultreturns itsTextContentat:168, not:184(:184is insideis_error_result).server.pycall sites are:1239and:1260, not:1186/:1207.None of these change the diagnosis.
The other report skills
weekly-reportandmonthly-reporthave the same hole — neither mentions auth failure, credentials, or a partial report. Deliberately out of scope here: their report structures differ enough that it is not the same edit, and each needs its own pins. They do now inherit the marker's documentation via_mureo-shared, which they all read as a prerequisite. Worth a follow-up issue.Also not touched: the
reports.dailyflag vocabulary in step 12 has no auth code, so a partial run's dashboard chip still has to go through{code: "custom"}. Adding a canonical code means touching the flag validator and the browser assets — separate change.Tests
TDD, red first. New
tests/test_auth_failure_envelope.pycovers the vocabulary, the classifier (including the negative cases: 400/404/429/500, a Google Adsmutate_error, an ordinaryRuntimeError), the envelope,is_auth_error_result, theis_error_resultmutation gate, andapi_error_handlerrouting. Newtests/test_daily_check_auth_failure.pypins the skill branch in both copies.tests/test_meta_ads_client.pygains the Meta 400-vs-auth classification, including that an ordinary 400 stays a plainRuntimeError.python -m pytest: 8893 passed, 12 failed. All 12 failures are the known local-environment set (locally-installed plugins leaking into tests that assume none — 513 tools vs 219 expected,smartnews_ads_*/line_ads_*in afrozenset()comparison — plus a locally-configured account scope):tests/analytics/builtin/test_live_clients.py(9),tests/test_mcp_server.py::TestListTools::test_list_tools_returns_all_tools,tests/test_mcp_server_plugin_wiring.py::test_no_plugins_is_additive_no_op,tests/test_mcp_tool_provider.py::test_default_discover_is_registry_and_yields_no_op_when_empty. None touch this change.ruff check,black --checkandruff format --checkclean on every file touched.mypyreports no error inmureo/core/auth_failure.py,mureo/mcp/_helpers.pyormureo/meta_ads/client.py(the 8 it does report are pre-existing missing-stubimport-untypederrors in unrelated files).Closes #580
Generated by Claude Code